/**
 * 
 */
package edu.ust.seis610.function;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;

import edu.ust.seis610.data.DataPoint;
import edu.ust.seis610.utils.NodeUtils;

/**
 * @author meeusen
 *
 */

@SuppressWarnings({"rawtypes","unchecked"})
public class Function implements Comparable
{
	private double fitnessValue;
	private HashMap equationMap;
	
//	default constructor lets instainiate random functions here for now, if we need a way to gen up non-random
//	tree structures later this could be moved to an alternate constructor
	public Function()
	{		
		this.equationMap=createRandomFunctionMap();	
	}
	
//	this constructor can be used for testing
	public Function(HashMap map)
	{
		this.equationMap=map;
	}
	
	private HashMap createRandomFunctionMap()
	{
		HashMap tmp = new HashMap();

		tmp.put("z1", NodeUtils.getRandomOperand());
		tmp.put("z2", NodeUtils.getRandomOperand());
		tmp.put("z3", NodeUtils.getRandomOperand());
		tmp.put("z4", NodeUtils.getRandomOperand());
		tmp.put("y1", NodeUtils.getRandomOperator());
		tmp.put("y2", NodeUtils.getRandomOperator());
		tmp.put("y3", NodeUtils.getRandomOperator());
		ensureThereAre2Xs(tmp);
		return tmp;	
	}
	
	private void ensureThereAre2Xs(HashMap tmp)
	{
		int xCnt=0;
		for(int x=1;x<=4;x++)
		{
			String str="z"+Integer.toString(x);
			TreeElement.Operand oper=(TreeElement.Operand)tmp.get(str.toString());
			if(oper.toString().equalsIgnoreCase("x"))
				xCnt++;
		}
		
		while (xCnt<2)
		{
			Random rand = new Random();
			int b = rand.nextInt(4);
			
			if(b==0)
				b++;
			
			String st = "z"+Integer.toString(b);
			TreeElement.Operand op=(TreeElement.Operand)tmp.get(st.toString());
			
			if(!(op.toString().equalsIgnoreCase("x")))
			{
				tmp.remove(st);
				tmp.put(st.toString(), TreeElement.Operand.X);
			}
			else
			{
				b++;
				String st2 = "z"+Integer.toString(b);
				tmp.remove(st2);
				tmp.put(st2.toString(),TreeElement.Operand.X);
			}
			xCnt++;
		}
	}
	
	private double solveFunction(double x)
	{	
		HashMap tmp = new HashMap();
//		the first thing we need to do is replace instances of TreeElement.Operand.X with the raw double value of x
		for(int a=1;a<=4;a++)
		{
			String str = "z"+Integer.toString(a);
			TreeElement.Operand oper =(TreeElement.Operand)this.equationMap.get(str);
//			if za is an Operand.X remove it from the map and add in the double value we are solving for
			if(oper.toString().equalsIgnoreCase("x"))
			{
				tmp.put(str.toString(),this.equationMap.get(str.toString()));
				this.equationMap.remove(str);
				this.equationMap.put(str,x);
			}	
		}
//		because we just switched out instances of Operand.X from the equation map and replaced them with raw double values,
//		we need to examine each occurrence of Operands in the map and see if their type is double or TreeElement.Operand before
//		using them in the equation
		double zz1,zz2,zz3,zz4;
		
		if((this.equationMap.get("z1"))instanceof TreeElement.Operand)
		{
			TreeElement.Operand z1 = (TreeElement.Operand)this.equationMap.get("z1");
			zz1=z1.eval();
		}
		else
			zz1=(Double)this.equationMap.get("z1");
		
		if((this.equationMap.get("z2"))instanceof TreeElement.Operand)
		{
			TreeElement.Operand z2 = (TreeElement.Operand)this.equationMap.get("z2");
			zz2=z2.eval();
		}
		else
			zz2=(Double)this.equationMap.get("z2");
		
		if((this.equationMap.get("z3"))instanceof TreeElement.Operand)
		{
			TreeElement.Operand z3 = (TreeElement.Operand)this.equationMap.get("z3");
			zz3=z3.eval();
		}
		else
			zz3=(Double)this.equationMap.get("z3");
		
		if((this.equationMap.get("z4"))instanceof TreeElement.Operand)
		{
			TreeElement.Operand z4 = (TreeElement.Operand)this.equationMap.get("z4");
			zz4=z4.eval();
		}
		else
			zz4=(Double)this.equationMap.get("z4");		
		
		TreeElement.Operator y1 = (TreeElement.Operator)this.equationMap.get("y1");
		TreeElement.Operator y2 = (TreeElement.Operator)this.equationMap.get("y2");
		TreeElement.Operator y3 = (TreeElement.Operator)this.equationMap.get("y3");		

		double y = (y3.eval(y2.eval((y1.eval(zz1, zz2)), zz3), zz4));

//		reset the values of x for next pass through
		this.equationMap.putAll(tmp);
		return y;
	}
	
//	This method iterates over the collection of training data and finds the fitness value for the function and sets it
	public void evaluateFitnessValue(ArrayList<DataPoint> trData)
	{
		double fit =0.0;
		for(DataPoint dp: trData)
		{
			double y=solveFunction(dp.getX());
			fit+=dp.getY()-y;
		}
//		set this function instance's fitness value to the absolute valute of the difference between the training data y and the function's y value
		this.setFitnessValue(Math.abs(fit));
	}
	
	public double getFitnessValue()
	{
		return fitnessValue;
	}

	public void setFitnessValue(double fitnessValue)
	{
		this.fitnessValue = fitnessValue;
	}
	
	public HashMap getEquationMap()
	{
		return equationMap;
	}

	public void setEquationMap(HashMap equationMap)
	{
		this.equationMap = equationMap;
	}
	
	public void print()
	{
		System.out.println(this.toString());
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 * Prints out the equation in this format: {[(z1y1z2)y2z3]y3z4}
	 */
	@Override
	public String toString()
	{
		String tmp = "";
		
		tmp +="{[("+this.equationMap.get("z1").toString()+this.equationMap.get("y1").toString()+this.equationMap.get("z2").toString()
			+")"+this.equationMap.get("y2").toString()+this.equationMap.get("z3").toString()+"]"+this.equationMap.get("y3").toString()
			+this.equationMap.get("z4").toString()+"}";
		return tmp;
	}
	
	@Override
	/**
	 * This will allow us to sort collections of Functions on their fitness values.  
	 * This will make ranking functions a snap.
	 */
	public int compareTo(Object aFunction)
	{
		if(!(aFunction instanceof Function))
			throw new ClassCastException("Tried to compare a non function object");
		
		if(this.getFitnessValue()> ((Function)aFunction).getFitnessValue())
			return 1;
		else if(this.getFitnessValue()<((Function)aFunction).getFitnessValue())
			return -1;
		else		
			return 0;
	}

}
